package gu.sql2java.manager.store;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.ByteBuffer;
import java.util.Map;

import com.google.common.base.Strings;
import com.google.common.base.Throwables;

import gu.sql2java.BaseBean;
import gu.sql2java.RowMetaData;
import gu.sql2java.TableManager;
import gu.sql2java.exception.ObjectRetrievalException;
import gu.sql2java.manager.Managers;
import gu.sql2java.store.BaseURLStore;
import gu.sql2java.store.ConditionChecks;
import gu.sql2java.store.DataNotFoundException;

import static com.google.common.base.Preconditions.*;
import static gu.sql2java.manager.utils.URLParseChecks.checkURLParse;
import static gu.sql2java.store.BinaryUtils.*;
import static gu.sql2java.store.ConditionChecks.checkNotNull;
import static gu.sql2java.store.ConditionChecks.checkTrue;

public class BaseColumnStore extends BaseURLStore {

	private final Handler handler = new Handler();

	protected final  String tablename;

	protected final String storeColumn;

	protected final TableManager<BaseBean> manager;

	protected final RowMetaData metaData;

	protected final Class<?> storeColumnType;

	protected final String protocol;

	protected final int extensionId;

	protected final int mimeId;

	protected final static String PKS = "PKS";
	
	public BaseColumnStore(String tablename,String storeColumn, String extensionColumn, String mimeColumn) {
		this.tablename = tablename;
		this.storeColumn = storeColumn;
		this.manager = Managers.managerOf(tablename);
		this.metaData = RowMetaData.getMetaData(tablename);
		this.extensionId = metaData.columnIDOf(extensionColumn);
		this.mimeId = metaData.columnIDOf(mimeColumn);
		this.storeColumnType = metaData.columnTypeOf(storeColumn);
		checkArgument(storeColumnType != null, "INVALID column %s", storeColumn);
		checkArgument(byte[].class.equals(storeColumnType) || ByteBuffer.class.isAssignableFrom(storeColumnType),
				"INVALID column type of %s,byte[] or java.nio.ByteBuffer required",storeColumn);
		checkArgument(extensionColumn == null || String.class.equals(metaData.columnTypeOf(this.extensionId)),
				"INVALID extensionColumn %s",extensionColumn);
		checkArgument(mimeColumn == null || String.class.equals(metaData.columnTypeOf(this.mimeId)),
				"INVALID mimeColumn %s",mimeColumn);
		this.protocol = tablename.replaceAll("_", "-");
	}

	@Override
	public String getProtocol() {
		return protocol;
	}
	protected Object[] primaryKeysOf(String md5, String extension){
		Map<String, Object> p = additionalParams.get();
		checkState(p != null && p.containsKey(PKS), "NOT PK defined");
		Object v = p.get(PKS);
		checkState(v instanceof Object[],"INVALID PK type,Object[] required");
		return (Object[])v;
	}
	protected final String pathOf(String suffix, Object... primaryKeys){
		StringBuffer buffer = new StringBuffer(storeColumn);
		for(int i=0; i < primaryKeys.length;++i){
			buffer.append("/").append(primaryKeys[i]);
		}
		if(!Strings.isNullOrEmpty(suffix)){
			buffer.append('.').append(suffix);
		}
		return buffer.toString();
	}
	protected final URL makeURL(String suffix, Object... primaryKeys){
		try {
			return new URL(getProtocol(),null,-1,pathOf(suffix,primaryKeys));
		} catch (MalformedURLException e) {
			throw new RuntimeException(e);
		}
	}
	/**
	 * 从数据库记录中获取当前数据的扩展名
	 * @param bean 数据库记录(不为{@code null})
	 * @return 扩展名
	 */
	protected String getExtension(BaseBean bean) {
		return bean.getValue(extensionId);
	}

	/**
	 * 从数据库记录中获取当前数据的MIME类型
	 * @param bean 数据库记录(不为{@code null})
	 * @return MIME类型
	 */
	protected String getMime(BaseBean bean) {
		String mime = bean.getValue(mimeId);
		String extension = bean.getValue(extensionId);
		if(mime != null){
			return mime;
		} else if( extension != null){
			mime = URLConnection.guessContentTypeFromName("." + extension);
			if(mime != null){
				return mime;
			}
		}
		return guessContentType(bean);
	}

	private String guessContentType(BaseBean bean) {
		try {
			if(bean == null){
				return null;
			}
			byte[] data = getBytes(bean.getValue(storeColumn));
			if(data == null){
				return null;
			}
			ByteArrayInputStream is = new ByteArrayInputStream(data);
			return URLConnection.guessContentTypeFromStream(is);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	protected final void fillStoreBean(BaseBean bean, byte[] binary, String extension) {
		if(ByteBuffer.class.equals(storeColumnType)){
			bean.setValue(storeColumn, ByteBuffer.wrap(binary));
		}else{
			bean.setValue(storeColumn, binary);	
		}
		if(extensionId >= 0){
			bean.setValue(extensionId, extension);
		}
		if(mimeId >= 0){
			bean.setValue(mimeId, getMime(bean));
		}
	}

	
	@Override
	protected URL doStore(byte[] binary, String md5, String extension, boolean overwrite, boolean makeURLOnly) throws IOException {
		try {
			Object[] pks = primaryKeysOf(md5,extension);
			BaseBean bean = manager.loadByPrimaryKeyChecked(pks);
			fillStoreBean(bean, binary, extension);
			if(!makeURLOnly){
				manager.save(bean);
			}
			return makeURL(extension, pks);
		} catch (Exception e) {
			Throwables.throwIfInstanceOf(e, IOException.class);
			throw new IOException(e);
		} finally{
			additionalParams.remove();
		}
	}

	@Override
	protected URL doFind(String md5) {
		return null;
	}

	@Override
	protected boolean doExists(URL storedURL) {
		DatabaseURLConnection connection = new DatabaseURLConnection(storedURL).parse();
		BaseBean found = manager.loadByPrimaryKey(connection.primaryKeys);
		return found != null && found.getValue(storeColumn) != null;
	}

	@Override
	protected boolean doDelete(URL storedURL) throws IOException {
		try {
			DatabaseURLConnection bean = new DatabaseURLConnection(storedURL).parse();
			return manager.deleteByPrimaryKey(bean.primaryKeys) == 1;
		} catch (Exception e) {
			Throwables.throwIfInstanceOf(e, IOException.class);
			throw new IOException(e);
		}
	}

	@Override
	protected URLStreamHandler doGetURLStreamHandler() {
		return handler;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = super.hashCode();
		result = prime * result + extensionId;
		result = prime * result + mimeId;
		result = prime * result + ((protocol == null) ? 0 : protocol.hashCode());
		result = prime * result + ((storeColumn == null) ? 0 : storeColumn.hashCode());
		result = prime * result + ((tablename == null) ? 0 : tablename.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (!(obj instanceof BaseColumnStore))
			return false;
		BaseColumnStore other = (BaseColumnStore) obj;
		if (extensionId != other.extensionId)
			return false;
		if (mimeId != other.mimeId)
			return false;
		if (protocol == null) {
			if (other.protocol != null)
				return false;
		} else if (!protocol.equals(other.protocol))
			return false;
		if (storeColumn == null) {
			if (other.storeColumn != null)
				return false;
		} else if (!storeColumn.equals(other.storeColumn))
			return false;
		if (tablename == null) {
			if (other.tablename != null)
				return false;
		} else if (!tablename.equals(other.tablename))
			return false;
		return true;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append(getClass().getSimpleName() + " [tablename=");
		builder.append(tablename);
		builder.append(", storeColumn=");
		builder.append(storeColumn);
		builder.append(", extensionColumn=");
		builder.append(metaData.columnNameOf(extensionId));
		builder.append(", mimeColumn=");
		builder.append(metaData.columnNameOf(mimeId));
		builder.append(", protocol=");
		builder.append(protocol);
		builder.append("]");
		return builder.toString();
	}

	protected class Handler extends URLStreamHandler{
	
		@Override
		protected URLConnection openConnection(URL u) throws IOException {
				return new DatabaseURLConnection(u);	
		}
		
	}
	protected class DatabaseURLConnection extends URLConnection{
		
		Object[] primaryKeys;

		protected DatabaseURLConnection(URL url) {
			super(url);
		}
		/**
		 * 解析URL为数据访问信息<br>
		 * 要求的URL格式 ${tablename}://${storeColumn}/${pk}...[/${pkN}]
		 * @return
		 * @throws URLParseException
		 */
		DatabaseURLConnection parse() throws URLParseException{
			checkTrue(getProtocol().equals(getURL().getProtocol()), URLParseException.class, 
					"INVALID protocol ,%s reqired",getProtocol());
			String[] components = url.getPath().split("/");
			try {
				checkTrue(components[0].equals(storeColumn), URLParseException.class, 
						"INVALID URL,the URL's path be reqired to start with '%s'",storeColumn);
				checkTrue(components.length == metaData.primaryKeyCount+1, URLParseException.class, 
						"MISMATCH path components acount, %s required",metaData.primaryKeyCount+1);
				String last = components[components.length-1];
				int lastDot = last.lastIndexOf(".");
				if(lastDot >= 0){
					// remove suffix if exists
					components[components.length-1] = last.substring(0, lastDot);
				}				
				primaryKeys = new Object[metaData.primaryKeyTypes.length];
				for(int i = 0; i < metaData.primaryKeyTypes.length;++i){
					primaryKeys[i] = valueOf(components[i+1], metaData.primaryKeyTypes[i]);
				}
			} catch (IndexOutOfBoundsException e) {
				throw new URLParseException("URL's path lack the necessary components /${storeColumn}/${pk}...[/${pkN}]", e);
			} catch (StringCastException e) {
				throw new URLParseException("FAIL TO get primary key from URL's path", e);
			}
			checkURLParse(url != null, "NOT DEFINED store column name in ref(#)");
			Class<?> columnType = checkNotNull(metaData.columnTypeOf(storeColumn),URLParseException.class,"INVALID column %s",storeColumn);
			checkURLParse(byte[].class.equals(columnType) || ByteBuffer.class.isAssignableFrom(columnType),
					"INVALID column type of %s, binary type ( byte[] or java.nio.ByteBuffer) required",storeColumn);
			return this;
		}

		@Override
		public void connect() throws IOException {			
			try {
				parse();
				connected = true;
			} catch (Exception e) {
				Throwables.throwIfInstanceOf(e, IOException.class);
				throw new IOException(e);
			}			
		}
		@Override
        public InputStream getInputStream() throws IOException{
            connect();
			try {
				BaseBean bean = manager.loadByPrimaryKeyChecked(primaryKeys);
				Object data = bean.getValue(storeColumn);
				ConditionChecks.checkTrue(data != null, DataNotFoundException.class, "column %s of %s is null", storeColumn, url.toString());
				return new ByteArrayInputStream(getBytes(data));
			} catch (ObjectRetrievalException e) {
				throw new DataNotFoundException(url,e);
			}catch (Exception e) {
				Throwables.throwIfInstanceOf(e, IOException.class);
				throw new IOException(e);
			}		
        }
	}
	@SuppressWarnings("unchecked")
	private static <T> T valueOf(String input,Class<T> targetType)throws StringCastException{
			if(Strings.isNullOrEmpty(input) || String.class.equals(targetType)){
				return (T) input;
			}
			checkNotNull(targetType != null, StringCastException.class,"target is null");
			try {
				return (T) targetType.getMethod("valueOf", String.class).invoke(null, input);
			} catch (IllegalAccessException | NoSuchMethodException | SecurityException e) {
			} catch (InvocationTargetException e) {
				throw new StringCastException(e.getTargetException());
			}
			throw new StringCastException(String.format("INVALID target type %s",targetType));
	}
	public static class URLParseException extends RuntimeException{
		private static final long serialVersionUID = 1L;

		public URLParseException(Throwable cause) {
			super(cause);
		}

		public URLParseException(String message, Throwable cause) {
			super(message, cause);
		}

		public URLParseException(String message) {
			super(message);
		}
	}
	public static class StringCastException extends RuntimeException{
		private static final long serialVersionUID = 1L;

		public StringCastException(Throwable cause) {
			super(cause);
		}

		public StringCastException(String message) {
			super(message);
		}
	}
}

